﻿using System;
using System.Data;
using Oracle.DataAccess.Client;

using BizElements.Core.ProviderComponents;

namespace BizElements.Core.Oracle
{
    /// <summary>
    /// IConnectionProvider implementation for Oracle databases.
    /// </summary>
    /// <remarks>Refrain from using this class in your code. Always use <see cref="IConnectionProvider"/> interface 
    /// instead. Only classes that implement <see cref="ICatalog"/> interface should be aware of concrete 
    /// IConnectionProvider implementation. <b>ICatalog</b> acts as a factory class for IConnectionProvider instances.</remarks>
    sealed class OracleConnectionProvider : IConnectionProvider
    {
        #region Members.

        DbmsType rdbms;
        IsolationLevel? defaultTransactionIsolationLevel;

        OracleConnection oraConn;
        OracleTransaction oraTran;

        bool isTransactionPending;
        bool isDisposed;

        #endregion

        #region Constructors.

        /// <summary>
        /// Constructor. Sets DBMS type and connection string.
        /// </summary>
        /// <param name="dbmsType">DBMS type to which this provider connects to. Only Oracle compatibile values are allowed.</param>
        /// <param name="connectionString">Connection string.</param>
        public OracleConnectionProvider(DbmsType dbmsType, string connectionString)
        {
            if (connectionString == null)
                throw new ArgumentNullException("connectionString", Messages.ConnectionProvider_ConnectionStringMayNotBeNull);
            if (!IsSupportedDbms(dbmsType))
                throw new ArgumentException(Messages.ConnectionProvider_UnsupportedDbmsType + dbmsType.ToString(), "dbmsType");

            this.rdbms = dbmsType;
            this.oraConn = new OracleConnection(connectionString);
        }

        /// <summary>
        /// Constructor. Sets DBMS type and connection string.
        /// </summary>
        /// <param name="dbmsType">DBMS type to which this provider connects to. Only Oracle compatibile values are allowed.</param>
        /// <param name="connectionString">Connection string.</param>
        /// <param name="defaultTransactionIsolationLevel">Isolation level for transactions started by the parameterless <see cref="BeginTransaction()"/> method overload.</param>
        public OracleConnectionProvider(DbmsType dbmsType, string connectionString,
            IsolationLevel defaultTransactionIsolationLevel)
            : this(dbmsType, connectionString)
        {
            this.defaultTransactionIsolationLevel = defaultTransactionIsolationLevel;
        }

        private static bool IsSupportedDbms(DbmsType dbmsType)
        {
            switch (dbmsType)
            {
                case DbmsType.Oracle_11g:
                case DbmsType.Oracle_10g:
                case DbmsType.Oracle_9i:
                    return true;

                default:
                    return false;
            }
        }

        #endregion

        #region Dispose.

        /// <summary>
        /// Releases the resources used by this object.
        /// Closes and disposes connection and transaction objects.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases the resources used by this object.
        /// </summary>
        private void Dispose(bool isDisposing)
        {
            // Check to see if Dispose has already been called.
            if (this.isDisposed == false)
            {
                if (isDisposing)
                {
                    // Dispose managed resources.
                    if (this.oraTran != null)
                    {
                        this.oraTran.Dispose();
                        this.oraTran = null;
                    }

                    // Closing the connection will abort (rollback) any pending transactions.
                    if (this.oraConn != null)
                    {
                        this.oraConn.Close();
                        this.oraConn.Dispose();
                        this.oraConn = null;
                    }
                }
            }

            this.isDisposed = true;
        }

        #endregion

        #region Methods.

        /// <summary>
        /// Opens the connection object.
        /// Throws exception if it's already open.
        /// </summary>
        public void OpenConnection()
        {
            if (this.IsOpen)
                throw new InvalidOperationException(Messages.ConnectionProvider_ConnectionIsAlreadyOpen);

            this.oraConn.Open();
            this.isTransactionPending = false;
        }

        /// <summary>
        /// Starts a new ADO.NET transaction using the open connection object of this class.
        /// Opens the connection if it isn't already open. 
        /// No nesting allowed, throws exception if the transaction is already pending.
        /// </summary>
        /// <remarks>Uses default <see cref="IsolationLevel"/>.</remarks>
        public void BeginTransaction()
        {
            if (this.defaultTransactionIsolationLevel != null)
            {
                BeginTransaction(this.defaultTransactionIsolationLevel.Value);
            }
            else
            {
                if (this.isTransactionPending)
                    throw new InvalidOperationException(Messages.ConnectionProvider_TranAlreadyPending);

                if (!IsOpen)
                    OpenConnection();

                this.oraTran = this.oraConn.BeginTransaction();
                this.isTransactionPending = true;
            }
        }

        /// <summary>
        /// Starts a new ADO.NET transaction using the open connection object of this class.
        /// Opens the connection if it isn't already open. 
        /// No nesting allowed, throws exception if the transaction is already pending.
        /// </summary>
        /// <param name="isolationLevel">The transaction isolation level.</param>
        public void BeginTransaction(IsolationLevel isolationLevel)
        {
            if (this.isTransactionPending)
                throw new InvalidOperationException(Messages.ConnectionProvider_TranAlreadyPending);

            if (!IsOpen)
                OpenConnection();

            this.oraTran = this.oraConn.BeginTransaction(isolationLevel);
            this.isTransactionPending = true;
        }


        /// <summary>
        /// Commits a pending transaction on the open connection object of this class.
        /// Throws exception if there is no pending transaction.
        /// </summary>
        public void CommitTransaction()
        {
            if (this.isTransactionPending == false)
                throw new InvalidOperationException(Messages.ConnectionProvider_ThereIsNoTranPending);

            if (!IsOpen)
                throw new InvalidOperationException(Messages.ConnectionProvider_ConnIsNotOpen);

            this.oraTran.Commit();

            this.isTransactionPending = false;
            this.oraTran.Dispose();
        }

        /// <summary>
        /// Rolls back a transaction from a pending state.
        /// Throws exception if there is no pending transaction.
        /// </summary>
        public void RollbackTransaction()
        {
            if (!IsOpen)
                throw new InvalidOperationException(Messages.ConnectionProvider_ConnIsNotOpen);

            if (this.isTransactionPending == false)
                throw new InvalidOperationException(Messages.ConnectionProvider_ThereIsNoTranPending);

            // Dispose transaction.
            this.oraTran.Rollback();
            this.isTransactionPending = false;
            this.oraTran.Dispose();
        }

        /// <summary>
        /// Closes the connection to the database. This is the preferred method of closing any open connection.
        /// </summary>
        /// <remarks>
        /// <para>The Close method rolls back any pending transactions. It then releases the connection 
        /// to the connection pool, or closes the connection if connection pooling is disabled. 
        /// It is a good practice to explicitly close connection immediately after it is no longer required.</para>
        /// <para>Does nothing if the connection has already been closed.</para>
        /// </remarks>
        public void CloseConnection()
        {
            CloseConnection(false);
        }

        /// <summary>
        /// Closes the open connection and commits or rollbacks transaction as specified.
        /// Does nothing if the connection has already been closed.
        /// </summary>
        /// <param name="commitPendingTransaction">True will commit the current transaction, 
        /// false will rollback the complete current transaction.</param>		
        private void CloseConnection(bool commitPendingTransaction)
        {
            // Nothing to close.
            if (!IsOpen)
                return;

            if (this.isTransactionPending)
            {
                if (commitPendingTransaction)
                    this.oraTran.Commit();
                else
                    this.oraTran.Rollback();

                this.isTransactionPending = false;
                this.oraTran.Dispose();
            }

            this.oraConn.Close();
        }

        #endregion

        #region Properties.

        /// <summary>
        /// Gets the DBMS type this provider is working with.
        /// </summary>
        public DbmsType DBMS
        {
            get { return this.rdbms; }
        }

        /// <summary>
        /// Gets the value which indicates whether there is a pending transaction.
        /// </summary>
        public bool IsTransactionPending
        {
            get { return this.isTransactionPending; }
        }

        /// <summary>
        /// Gets the value which indicates whether the connection is open.
        /// </summary>
        public bool IsOpen
        {
            get
            {
                if ((this.oraConn.State & ConnectionState.Open) > 0)
                    return true;
                else
                    return false;
            }
        }

        /// <summary>
        /// Gets the connection object.
        /// </summary>
        public IDbConnection Connection
        {
            get { return this.oraConn; }
        }

        /// <summary>
        /// Gets the transaction object.
        /// </summary>
        public IDbTransaction Transaction
        {
            get { return this.oraTran; }
        }

        /// <summary>
        /// Gets a value indicating whether the connection provider has been disposed of.
        /// </summary>
        public bool IsDisposed
        {
            get { return this.isDisposed; }
        }

        #endregion
    }
}